﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition.Hosting;
using System.IO;
using System.Linq;
using ExcelAutomator.Configuration;
using System.ComponentModel.Composition;
using ExcelAutomator.Validation;
using log4net;
using log4net.Appender;
using Microsoft.Office.Interop.Excel;

namespace ExcelAutomator
{
	public class ExcelAutomationService : IDisposable
	{
		private readonly IDictionary<string, FileInfo> m_InputFiles;
		private FileInfo m_TargetFile;
		private ExcelAutomatorConfiguration m_Configuration;
		private readonly IEnumerable<ExecutorSet> m_ExecutorSets;
		private CompositionContainer m_Container;

		#region ParameterPatternParser
		
		private IParameterPatternParser m_ParameterPatternParser;
		private IParameterPatternParser ParameterPatternParser
		{
			get
			{
				if (m_ParameterPatternParser == null)
				{
					m_ParameterPatternParser = m_Container.GetExport<IParameterPatternParser>().Value;
				}
				return m_ParameterPatternParser;
			}
		}

		#endregion

		#region InputFiles

		public ReadOnlyCollection<string> InputFiles
		{
			get
			{
				return new ReadOnlyCollection<string>(m_InputFiles.Values.Select(x=>x.FullName).ToList());
			}
		}

		#endregion

		#region TargetFiles

		#region TargetFiles

		public ReadOnlyCollection<string> TargetFiles
		{
			get
			{
				return new ReadOnlyCollection<string>(ExecutionContexts.Select(x=>x.TargetFile.FullName).ToList());
			}
		}

		#endregion

		#endregion

		#region ExecutorSets

		internal ReadOnlyCollection<ExecutorSet> ExecutorSets
		{
			get
			{
				return new ReadOnlyCollection<ExecutorSet>(m_ExecutorSets.ToList());
			}
		}

		#endregion

		#region ExecutionContexts

		private IList<ExecutionContext> m_ExecutionContexts;
		internal IEnumerable<ExecutionContext> ExecutionContexts
		{
			get
			{
				if (m_ExecutionContexts == null)
				{
					m_ExecutionContexts = new List<ExecutionContext>(CreateExecutionContexts(
						m_InputFiles, m_TargetFile, m_Configuration.Settings)); 
				}
				return m_ExecutionContexts;
			}
		}

		#endregion

		#region Application

		private Application m_Application;
		private Application Application
		{
			get
			{
				if (m_Application == null)
				{
					m_Application = new Application{};
					m_Application.DisplayAlerts = false;
					m_Application.EnableEvents = false;
				}
				return m_Application;
			}
			
		}

		#endregion

		#region ValidationEngine

		private ValidationEngine m_ValidationEngine;
		private ValidationEngine ValidationEngine
		{
			get
			{
				if (m_ValidationEngine == null)
				{
					m_ValidationEngine = new ValidationEngine();
				}
				return m_ValidationEngine;
			}

		}

		#endregion

		#region Ctor

		public ExcelAutomationService(ExcelAutomatorConfiguration configuration)
		{
			#region pre
			if (configuration == null)
			{
				throw new ArgumentNullException("configuration");  //NON-NLS-1
			}
			#endregion
			
			SetupPluginContainer();
			m_InputFiles = DetermineInputFiles(configuration);
			m_TargetFile = DetermineTargetFile(configuration);
			if (m_TargetFile != null) EnsureTargetFileIsNotInInputList(configuration, m_InputFiles, m_TargetFile);
			m_ExecutorSets = ResolveExecutorSets(configuration);
			m_Configuration = configuration;
			
		}
		#endregion

		#region DetermineTargetFile

		private static FileInfo DetermineTargetFile(ExcelAutomatorConfiguration cfg)
		{
			if (!cfg.Settings.Target.IsNOE())
			{
				if (File.Exists(cfg.Settings.Target))
				{
					return new FileInfo(cfg.Settings.Target);
				}
			}
			return null;
		}
		#endregion

		#region EnsureTargetFileIsNotInInputList

		private static void EnsureTargetFileIsNotInInputList(ExcelAutomatorConfiguration cfg, IDictionary<string, FileInfo> inputFiles, FileSystemInfo targetFile)
		{
			#region pre
			if (targetFile == null)
			{
				throw new ArgumentNullException("targetFile");  //NON-NLS-1
			}

			#endregion

			if (!cfg.Settings.Target.Equals(cfg.Settings.Input))
			{
				//ensure target will not be in the input list!
				if (inputFiles.ContainsKey(targetFile.FullName))
				{
					inputFiles.Remove(targetFile.FullName);
				}
			}
		}

		#endregion

		#region CreateExecutionContexts

		private IEnumerable<ExecutionContext> CreateExecutionContexts(IDictionary<string, FileInfo> inputFiles, FileInfo targetFile, SettingsConfiguration configuration)
		{
			var result = new List<ExecutionContext>();

			foreach (FileInfo inputFile in inputFiles.Values)
			{
				if (targetFile == null)
				{
					result.Add(new ExecutionContext(Application, inputFile, inputFile, configuration, ParameterPatternParser));
				}
				else
				{
					result.Add(new ExecutionContext(Application, inputFile, targetFile, configuration, ParameterPatternParser));
				}
			}
			return result;
		}

		#endregion

		#region ResolveExecutorSets

		private IList<ExecutorSet> ResolveExecutorSets(ExcelAutomatorConfiguration configuration)
		{
			var executorSets = new List<ExecutorSet>();
			var factory = new ExecutorFactory(m_Container);

			foreach (var executionSet in configuration.ExecutionSets)
			{
				var set = new ExecutorSet(executionSet.Id);
				
				foreach (var executorCfg in executionSet.Executors)
				{
					IExecutor executor;
					if (factory.TryCreate(executorCfg, out executor))
					{
						executor.Configure(executorCfg.XmlConfiguration);
						set.Add(executor);
					}
					else
					{
						Log.Error(string.Format("Couldn't create executor."));
					}
				}
				executorSets.Add(set);
			}
			return executorSets;
		}

		#endregion

		#region DetermineInputFiles

		private IDictionary<string, FileInfo> DetermineInputFiles(ExcelAutomatorConfiguration cfg)
		{
			var sourceFiles = new Dictionary<string, FileInfo>();
			var input = Path.IsPathRooted(cfg.Settings.Input)
			            	? cfg.Settings.Input
			            	: Path.Combine(Environment.CurrentDirectory, cfg.Settings.Input);

			#region Validation

			if (!Directory.Exists(input) && !File.Exists(input))
			{
				ValidationEngine.SetCustomViolationMessage(string.Format("The file/directory '{0}' doesn't exists", input));
			}
			else
			{
				ValidationEngine.Validate.That.IO.Directory(input)
					.DirectoryExists()
					.ContainsFilesWithExtensions(cfg.Settings.SearchPattern);

				ValidationEngine.Validate.That.IO.File(input)
					.FileExists();
			}

			#endregion

			if (Directory.Exists(input))
			{
				var directory = new DirectoryInfo(Path.GetFullPath(cfg.Settings.Input));

				foreach (var searchPattern in cfg.Settings.SearchPattern)
				{
					foreach (var file in directory.GetFiles(searchPattern,
																									cfg.Settings.ScanSubFolders ?
																									SearchOption.AllDirectories :
																									SearchOption.TopDirectoryOnly))
					{
						if (!sourceFiles.ContainsKey(file.FullName))
						{
							sourceFiles.Add(file.FullName, file);
						}
					}
				}
			}
			else if(File.Exists(input))
			{
				var file = new FileInfo(cfg.Settings.Input);
				sourceFiles.Add(file.FullName, file);
			}
			return sourceFiles;
		}

		#endregion

		#region SetupPluginContainer
		
		private void SetupPluginContainer()
		{
			var catalog = new AggregateCatalog(
				new DirectoryCatalog(Environment.CurrentDirectory, "*.dll"));
			m_Container = new CompositionContainer(catalog);
			m_Container.ComposeParts();
		}

		#endregion

		#region Validate (out ValidationReport report)

		public bool Validate(out ValidationReport report)
		{
			m_PrintHeaderAction(m_Configuration, m_InputFiles.Values, true);
			report = m_ValidateAction(ExecutionContexts, ExecutorSets, ValidationEngine);
			DisposeExcel();
			return report.IsValid;
		}

		#endregion

		#region Validate

		public bool Validate()
		{
			ValidationReport report;
			return Validate(out report);
		}

		#endregion

		#region ValidateAction

		private readonly Func<IEnumerable<ExecutionContext>, IEnumerable<ExecutorSet>, ValidationEngine, ValidationReport>
			m_ValidateAction = (executionContexts, executorSets, validationEngine) =>
       	{
       		foreach (var context in executionContexts)
       		{
       			foreach (ExecutorSet executorSet in executorSets)
       			{
       				foreach (IExecutor executor in executorSet.Executors)
       				{
       					try
       					{
									executor.Validate(validationEngine, context);
       					}
       					catch (Exception e)
       					{
									Log.Error(e.Message + " " + e.InnerException.Message, e);
       					}
       				}
       			}
       		}

       		var report = validationEngine.GenerateReport();
       		Log.InfoFormat("Validate Configuration -> {0}", report.IsValid == true ? "Success" : "Failure");
       		if (!report.IsValid)
       		{
       			for (int i = 0; i < report.Violations.Count(); i++)
       			{
							Log.ErrorFormat("{0}: {1}", i + 1, report.Violations.ElementAt(i));
       			}
       		}
       		return report;
       	};

		

		#endregion

		#region ExecuteAutomationAction

		private readonly Action<IEnumerable<ExecutionContext>, IEnumerable<ExecutorSet>, ValidationReport> m_ExecuteAutomationAction =
			(executionContexts, executorSets, report) =>
				{
					if (report.IsValid)
					{
						try
						{
							for (int i = 0; i < executionContexts.Count(); i++)
							{
								Log.InfoFormat("--- Processing File {0}/{1} ---", i + 1, executionContexts.Count());
								ProcessExecutionSets(executionContexts.ElementAt(i), executorSets);
								Log.InfoFormat("--- End of File {0} --- ", i + 1);
								Log.Info("");
							}
						}
						catch (Exception e)
						{
							Log.Error(e.Message + " " + e.InnerException.Message, e);
						}
					}
					else
					{
						Log.WarnFormat(
							"Can't execute automation, the configuration provided is not valid. Please check the validation results.");
					}
				};

		

		#endregion

		#region PrintHeaderAction

		private Action<ExcelAutomatorConfiguration, IEnumerable<FileInfo>, bool> m_PrintHeaderAction =
			(config, inputFiles, validationMode) =>
				{
					Log.InfoFormat("Starting Excel Automation {0}...", validationMode == true ? "- VALIDATION MODE" : string.Empty);
					Log.InfoFormat("Input File/Folder: {0}", config.Settings.Input);
					Log.InfoFormat("Seach Pattern: {0}", string.Join("; ", config.Settings.SearchPattern.ToArray()));
					Log.InfoFormat("Scan Sub-Folders: {0}", config.Settings.ScanSubFolders);
					Log.InfoFormat("Total Files to process: {0}", inputFiles.Count());

					var fileAppender = LogManager.GetRepository().GetAppenders().OfType<FileAppender>().FirstOrDefault();

					if(fileAppender != null)
					{
						Log.InfoFormat("Log File: {0}", fileAppender.File);
					}

					if (config.Settings.IsOutDirectoryDefined)
					{
						Log.InfoFormat("Output Folder: {0}", config.Settings.OutputFolder);
					}

					if (config.Settings.FilenamePattern != null)
					{
						//TODO HACK: The Value might result in strangr output, coz the ExecutionContext might parse parameters before
						Log.InfoFormat("Filename Pattern: {0}", config.Settings.FilenamePattern);
					}

					Log.Info("");
				};

		#endregion

		#region ExecuteAutomation

		public void ExecuteAutomation()
		{
			m_PrintHeaderAction(m_Configuration, m_InputFiles.Values, false);
			var report = m_ValidateAction(ExecutionContexts, ExecutorSets, ValidationEngine);
			m_ExecuteAutomationAction(ExecutionContexts, ExecutorSets, report);
			DisposeExcel();
		}

		#endregion

		#region ProcessExecutionSets

		private static void ProcessExecutionSets(ExecutionContext context, IEnumerable<ExecutorSet> executorSets)
		{
			Log.InfoFormat("Input File: {0}", context.InputFile.FullName);
			Log.InfoFormat("Target File: {0}", context.TargetFile.FullName);

			foreach (var executorSet in executorSets)
			{
				Log.InfoFormat("ExecutionSet {0}: ", executorSet.Id);
							
				foreach (var executor in executorSet.Executors)
				{
					var result = executor.Execute(context);
					Log.InfoFormat(" -> {0}<{1}>: {2}", executor.Type, executor.ActionId, result);

					if (!result.Success)
					{
						Log.Error(string.Format("Procssing failed."));
						Log.Warn("--- Goto next input file. ---");
						return;
					}
				}
			}
			context.SaveAndClose();
		}

		#endregion

		#region Destructor

		~ExcelAutomationService()
		{
			DisposeExcel();
		}

		#endregion

		#region Log

		private static ILog Log
		{
			get
			{
				return LogManager.GetLogger(typeof (ExcelAutomationService));
			}
		}

		#endregion

		#region Dispose

		public void Dispose()
		{
			DisposeExcel();
			m_ExecutionContexts.Clear();
			m_InputFiles.Clear();
			m_TargetFile = null;
			m_Configuration = null;
		}

		#endregion

		#region DisposeExcel

		private void DisposeExcel()
		{
			if (Application != null)
			{
				if (Application.Workbooks.Count > 0)
				{
					foreach (Workbook workbook in Application.Workbooks)
					{
						workbook.Close(false, Type.Missing, Type.Missing);
					}
				}
				Application.Quit();
			}
		}

		#endregion

	}
}
